package nachos.userprog;

import nachos.machine.*;
import nachos.threads.*;
import nachos.userprog.*;

/**
 * Provides a simple, synchronized interface to the machine's console. The
 * interface can also be accessed through <tt>OpenFile</tt> objects.
 */
public class SynchConsole
{
    /**
     * Allocate a new <tt>SynchConsole</tt>.
     *
     * @param    console    the underlying serial console to use.
     */
    public SynchConsole(SerialConsole console)
    {
        this.console = console;

        Runnable receiveHandler = new Runnable()
        {
            public void run()
            {
                receiveInterrupt();
            }
        };
        Runnable sendHandler = new Runnable()
        {
            public void run()
            {
                sendInterrupt();
            }
        };
        console.setInterruptHandlers(receiveHandler, sendHandler);
    }

    /**
     * Return the next unsigned byte received (in the range <tt>0</tt> through
     * <tt>255</tt>). If a byte has not arrived at, blocks until a byte
     * arrives, or returns immediately, depending on the value of <i>block</i>.
     *
     * @param    block    <tt>true</tt> if <tt>readByte()</tt> should wait for a
     * byte if none is available.
     * @return the next byte read, or -1 if <tt>block</tt> was <tt>false</tt>
     * and no byte was available.
     */
    public int readByte(boolean block)
    {
        int value;
        boolean intStatus = Machine.interrupt().disable();
        readLock.acquire();

        if (block || charAvailable)
        {
            charAvailable = false;
            readWait.P();

            value = console.readByte();
            Lib.assertTrue(value != -1);
        }
        else
        {
            value = -1;
        }

        readLock.release();
        Machine.interrupt().restore(intStatus);
        return value;
    }

    /**
     * Return an <tt>OpenFile</tt> that can be used to read this as a file.
     *
     * @return a file that can read this console.
     */
    public OpenFile openForReading()
    {
        return new File(true, false);
    }

    private void receiveInterrupt()
    {
        charAvailable = true;
        readWait.V();
    }

    /**
     * Send a byte. Blocks until the send is complete.
     *
     * @param    value    the byte to be sent (the upper 24 bits are ignored).
     */
    public void writeByte(int value)
    {
        writeLock.acquire();
        console.writeByte(value);
        writeWait.P();
        writeLock.release();
    }

    /**
     * Return an <tt>OpenFile</tt> that can be used to write this as a file.
     *
     * @return a file that can write this console.
     */
    public OpenFile openForWriting()
    {
        return new File(false, true);
    }

    private void sendInterrupt()
    {
        writeWait.V();
    }

    private boolean charAvailable = false;

    private SerialConsole console;
    private Lock readLock = new Lock();
    private Lock writeLock = new Lock();
    private Semaphore readWait = new Semaphore(0);
    private Semaphore writeWait = new Semaphore(0);

    private class File extends OpenFile
    {
        File(boolean canRead, boolean canWrite)
        {
            super(null, "SynchConsole");

            this.canRead = canRead;
            this.canWrite = canWrite;
        }

        public void close()
        {
            canRead = canWrite = false;
        }

        public int read(byte[] buf, int offset, int length)
        {
            if (!canRead)
                return 0;

            int i;
            for (i = 0; i < length; i++)
            {
                int value = SynchConsole.this.readByte(false);
                if (value == -1)
                    break;

                buf[offset + i] = (byte) value;
            }

            return i;
        }

        public int write(byte[] buf, int offset, int length)
        {
            if (!canWrite)
                return 0;

            for (int i = 0; i < length; i++)
                SynchConsole.this.writeByte(buf[offset + i]);

            return length;
        }

        private boolean canRead, canWrite;
    }
}
